module DryScaffold
#  @@scaffold_template_dir = "#{File.dirname(__FILE__)}/../views"
  class ScaffoldTable #:nodoc:
    def initialize(klass, options)
      
    end
  end
  
  class ScaffoldColumn #:nodoc:
   
    attr_reader :ajax, :cols, :description, :filterable, :html_options, :in_edit, :in_list, :in_new, :in_show, :max_group_by, :max_length, :name, :rows, :size_in_edit, :size_in_list, :sortable, :sort_order, :sort_sql, :sub_type, :type
    attr_writer :sort_order
    
    def initialize(klass, options) #:nodoc:
      @name = options[:name]
      @sub_type = options[:sub_type].nil? ? nil : options[:sub_type]
      @in_list = options[:in_list].nil? ? true : options[:in_list]      
      @in_edit = options[:in_edit].nil? ? true : options[:in_edit]      
      @in_new = options[:in_new].nil? ? @in_edit : options[:in_new]      
      @in_show = options[:in_show].nil? ? @in_edit : options[:in_show]      
      @label = options[:label].nil? ? Inflector.titleize(@name) : options[:label]      
      @sortable = options[:sortable].nil? ? true : options[:sortable]      
      @sort_sql = options[:sort_sql].nil? ? klass.table_name + "." + @name : options[:sort_sql] unless !@sortable
      @ajax = options[:ajax].nil? ? false : options[:ajax]
      @description = options[:description].nil? ? '' : options[:description]
      @filterable = options[:filterable].nil? ? true : options[:filterable]
      @max_group_by = options[:max_group_by].nil? ? '' : options[:max_group_by]
      @sort_order = options[:sort_order].nil? ? '' : options[:sort_order]
      @size_in_list = options[:size_in_list].nil? ? 15 : options[:size_in_list]
      @size_in_edit = options[:size_in_edit].nil? ? 40 : options[:size_in_edit]
      @max_length = options[:max_length].nil? ? @size_in_edit : options[:max_length]
      @rows = options[:rows].nil? ? 6 : options[:rows]
      @cols = options[:cols].nil? ? 40 : options[:cols]
      @html_options = options[:html_options].nil? ? {} : options[:html_options]

      
      if (options[:except])
        @in_edit    = @in_new = !options[:except].include?(:edit)
        @in_new     = !options[:except].include?(:new) if options[:except].include?(:new)
        @in_list    = !options[:except].include?(:list)
        @in_show    = !options[:except].include?(:show)
        @filterable = !options[:except].include?(:filterable)
        @sortable   = !options[:except].include?(:sortable)
      end
      if options.has_key?(:type)
        @type = options[:type]
      elsif name.include?("phone") || name == "fax"
        if name.include?("ext")
          @type = :phone_ext
          if @in_edit != false
            @size_in_edit = 10
            @max_length = 10
          end
        else
          @type = :phone
          if @in_edit != false
            @size_in_edit = 12
            @max_length = 12
          end
        end
      else
        @type = klass.columns_hash[options[:name]].class
      end
    end
    
    def sortable? 
      @sortable
    end
    
    def viewable?
      @name != "created_at" and @name != "updated_at" and @name != "lock_version"
    end
    
    def viewable_in_edit?(action = "edit")
#ActionController::Base::logger.debug "xxxxx#{action.to_s}xxn#{@in_new}xxe#{@in_edit}x"
      if action.to_s == "edit"
        @in_edit
      elsif action.to_s == "new"
        @in_new
      else
        false
      end
    end

    def viewable_in_list?
      @in_list
    end

    def viewable_in_show?
      @in_show
    end

    def display_type
      type
    end

    def in_list_query? #:nodoc:
      @in_list || @max_group_by ? true : false
    end

    def label
      @label ? @label : ActiveRecord::Base.human_attribute_name(@name)
    end

  end
  
  module Model #:nodoc:
    module ClassMethods #:nodoc:
  
    def build_scaffold_table
      ScaffoldTable.new(self, { :sortable => true })
    end    

    def build_scaffold_columns
      scaffold_columns = Array.new
      content_columns.each do |column|
        scaffold_columns << ScaffoldColumn.new(self, { :name => column.name })
      end 
      scaffold_columns
    end    
    
    def build_scaffold_columns_hash
      scaffold_columns_hash = Hash.new
      scaffold_columns.each do |scaffold_column|
        scaffold_columns_hash[scaffold_column.name] = scaffold_column
      end
        scaffold_columns_hash
      end
    end
  end

  module Helper #:nodoc:
  end
  
  module Common #:nodoc:
    def current_sort(params)
      session[params[:scaffold_id]][:sort] rescue ''
    end
  
    def current_sort_direction(params)
      session[params[:scaffold_id]][:sort_direction] rescue ''
    end  
  end 
  
  module Controller #:nodoc:
    include DryScaffold::Common
  
    def clear_flashes 
    end
  
    def store_or_get_from_session(id_key, value_key)
      session[id_key][value_key] = params[value_key] if !params[value_key].nil?
      params[value_key] ||= session[id_key][value_key]
    end
  
    def update_params(options)
      @scaffold_id = params[:scaffold_id] ||= options[:default_scaffold_id]
      session[@scaffold_id] ||= {:sort => options[:default_sort], :sort_direction => options[:default_sort_direction], :page => 1}
    
      store_or_get_from_session(@scaffold_id, :sort)
      store_or_get_from_session(@scaffold_id, :sort_direction)
      store_or_get_from_session(@scaffold_id, :page)
    end
    
  end
  
end

class ActiveRecord::Base
  extend DryScaffold::Model::ClassMethods

  def self.columns_into_sql(options = {}) #:nodoc:
    sql = ""
    for column in @by_model.scaffold_columns
  	# for col_name in @by_model.column_order 
  	# 			column = @by_model.columns_hash[col_name]
			if column.in_list_query?
			  sql << "," if sql.length > 0
        if column.max_group_by
          sql << "MAX("
        end
				sql << @by_model.table_name + "." + column.name
        if column.max_group_by
          sql << ")"
        end
				sql << " as " + column.name
	 	  end 
		end
		sql
  end

  def self.find_by_def(options = {}) #:nodoc:
    @by_model = self
  	@sql_select = columns_into_sql
    if options[:include_children]
    	self.reflect_on_all_associations.to_a.each do |assoc|
        @by_model = assoc.name.to_s.camelize.singularize.constantize
      	@sql_select << "," + columns_into_sql
      	@sql_include = [] if @sql_include.nil?
    	  @sql_include << assoc.name
      end
    end
    options.delete(:include_children)
    options[:include] = @sql_include
  	options[:select] = @sql_select
    join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
    options[:from] = self.table_name + join_dependency.join_associations.collect{|join| join.association_join }.join
    options[:group] = self.table_name + ".id" if @sql_select.include?("MAX(")
    find_by_sql construct_finder_sql(options)
  end

  # Returns a display worthy label of the Model.
  def self.label
    @label ? @label : name.titleize.split.last.pluralize
  end

  # Give it a name other than the name of the Model.
  #
  # Example:
  #   known_to_humans_as "Workshop"
  #
  def self.known_to_humans_as(value)
    @label = value
  end  

  # Customize your confirm message in the javascript popup on delete globally in environment.rb or by calling this function
  # in your Model.
  #
  # Example in Model:
  #   confirm_message_on_delete "Are you really sure?"
  #
  # Example in environment.rb:
  #   module DryScaffoldEngine
  #     config :ajax_filter_fields, true
  #     config :confirm_message_on_delete, "Are you sure?"
  #   end  
  def self.confirm_message_on_delete(value)
    @on_delete_message = value
  end
  
  # Confirm message used in popup window on delete.
  #
  # Example in Model:
  #   Company.on_delete_message
  #
  def self.on_delete_message
    message = @on_delete_message
    message ||= DryScaffoldEngine.config(:confirm_message_on_delete) if !DryScaffoldEngine.config(:confirm_message_on_delete).nil? and DryScaffoldEngine.config(:confirm_message_on_delete)
    message ||= 'Are you sure?'
    message
  end
  
  
  # Identifies which columns should be managed and how to manage them. The <tt>name</tt> of the column in the Model is required.
  # 
  # Originally, I leveraged ajax_scaffold's +eval+ mechanism for customizing display, edit and filtering. But it was messy and
  # not easily extended. So, goodbye to <tt>:eval...</tt> options. Welcome to +Override Methods+. +Override Methods+ use the
  # followin convention <tt>#{column.name}_custom_default_filter</tt>.
  #
  # +Override Methods+:
  # * <tt>#{column.name}_custom_calculate_filter</tt> - If supplied, allows custom filter calculation. For example, setting
  #   the conditions for multilple columns. See example below.
  # * <tt>#{column.name}_custom_default_filter</tt> - If supplied, will assign an initial default filter and on +Reset+. This may
  #   go in the Model or Controller.
  # * <tt>#{column.name}_custom_edit_field</tt> - If supplied, will be used for the HTML field on +edit+. This should go in
  #   Helper. If the field is of <tt>:type => :lookup</tt>, this method should provide a list of options. See example below.
  # * <tt>#{column.name}_custom_filter_field</tt> - If supplied, will be used as the filter field. This should go in
  #   Helper. If the field is of <tt>:type => :lookup</tt>, this method should provide a list of options. See example below.
  # * <tt>#{column.name}_custom_list_field</tt> - If supplied, will be used for the value to display in the list. This may
  #   go in the Model or Helper. 
  # * <tt>#{model_name}_custom_restrict_to_filter</tt> - If supplied, will assign a restrict to filter. This value will take 
  #   precedence over all filter values. This may go in the Model or Controller.
  #
  # Options are:
  # * <tt>:ajax</tt> - Default value is +false+. If set to +true+ then in-line AJAX edit is enabled for this column.
  # * <tt>:description</tt> - If supplied, the value is displayed after the edit field when editing.
  # * <tt>:filterable</tt> - Default value is +false+.
  # * <tt>:html_options</tt> - HTML options to be used in helper functions. Example: value is +{:range => [1900, Date.today.year]}+.
  # * <tt>:in_edit</tt> - Show this column on Edit. Default value is +true+.
  # * <tt>:in_list</tt> - Show this column in the List view. Default value is +false+.
  # * <tt>:in_show</tt> - Show this column in the Show view. Default value is the value of <tt>:in_edit</tt>.
  # * <tt>:label</tt>
  # * <tt>:max_length</tt> - Maximum length to allow in text field. <tt>text_field ... :maxlength => column.max_length</tt>.
  # * <tt>:sortable</tt> - Default value is +false+. May also assign an initial value of <tt>:asc</tt> or <tt>:desc</tt>.
  # * <tt>:sub_type</tt> - Currently only applies when <tt>:type => :date</tt>.
  # * <tt>:type</tt> - Default value is a text field.
  #   * <tt>:country</tt>
  #   * <tt>:custom</tt> - Assumes the following methods are defined in the +Helper+ file: <tt>#{column.name}_custom_edit_field</tt>,
  #     <tt>#{column.name}_custom_filter_field</tt>, <tt>#{column.name}_custom_list_field</tt>.
  #   * <tt>:date</tt> - if the DhtmlCalendar Plugin is present you can specify the following <tt>:sub_type =></tt>:
  #     * <tt>:box</tt> - Use the DateBocks controls.
  #     * <tt>:field</tt> - Use a text field.
  #     * <tt>:flat</tt> - Calendar always displayed.
  #     * <tt>:popup</tt> - Use an icon to click on only.
  #     * <tt>:select</tt> - Use Rails like drop down controls (Combo-boxes).
  #   * <tt>:filter_only</tt> - A filter only column. To have two columns referencing the same column in the DB, add attr_accessor
  #     for the <tt>:filter_only</tt> column. See example below.
  #   * <tt>:lookup</tt> - Expects to receive options for the select from <tt>#{column.name}_custom_select_options</tt> and 
  #     in <tt>#{column.name}_custom_filter_field</tt> if <tt>filterable = true</tt>. See example below.
  #   * <tt>:password</tt>
  #   * <tt>:phone</tt>
  #   * <tt>:usa_state</tt>
  #   * <tt>:text_area</tt>
  #       <tt>:cols</tt>
  #       <tt>:rows</tt>
  #   * <tt>:timezone</tt>
  #
  # Controller Examples using optional 1.0 syntax:
  #
  #   Company.has_children [:contacts, :comments]
  #   Company.has_crudability :viewable => "Contacts"  
  #   Company.has_columns [
  #     {:name => "id",
  #       :except => [:edit],
  #       :size_in_list => 6 },
  #     {:name => "name",
  #       :ajax => true },
  #     {:name => "phone",
  #       :description => "(Format: ###-###-####)", 
  #       :size_in_list => 10 },
  #     {:name => "company_type_id",
  #       :type => :lookup },
  #     {:name => "count",
  #       :except => [:edit, :filterable, :sortable] }
  #   ]  
  #
  # Model Examples using 0.2 syntax:
  # 
  #   attr_accessor :user_id_2_ajax
  #
  #   has_column "id",{
  #     :in_list => 6,
  #     :in_edit => false,
  #     :filterable => true,
  #     :sortable => true
  #   }
  #
  #   has_column "name",{
  #     :ajax => true,
  #     :in_list => true,
  #     :sortable => :desc,
  #     :filterable =>true
  #   }
  #
  #   has_column "phone",{
  #     :description => "(Format: ###-###-####)", 
  #     :in_list => 10,
  #     :sortable => true,
  #     :filterable =>true
  #   }
  #
  #   has_column "company_type_id",{
  #      :filterable => true,
  #      :in_list => true,
  #      :sortable => true,
  #      :type => :lookup
  #    }
  #
  #   has_column "count", {
  #     :in_edit => false,
  #     :in_list => true
  #   }
  #
  #   has_column "name_key", {
  #     :label => "English Name",
  #     :filterable => true,
  #     :in_list => true,
  #     :sortable => false,
  #     :type => :custom
  #   }
  #
  #   has_column "user_id_2", {
  #     :label => "Client Contact",
  #     :in_list => false,
  #     :type => :lookup,
  #     :sortable => false
  #   }
  #   has_column "user_id_2_ajax", {
  #     :label => "Client Contact",
  #     :filterable => true, 
  #     :in_edit => false,
  #     :in_list => 10,
  #     :type => :filter_only,
  #     :sortable => false
  #   }
  #
  # Model Examples when overriding value displayed in the list. Put the method in the Model when accessing model attributes only:
  # 
  #   def company_type_id_custom_list_field
  #     company_type.name
  #   end
  #
  #   def user_id_custom_list_field
  #     coordinator.last_name + ', ' + coordinator.first_name + ' (' + coordinator.login + ')'    
  #   end
  #
  # Helper Example when overriding value displayed in the list. Put the method in the Helper when you need to access helper methods:
  #
  #   def user_id_custom_edit_field(one_row)
  #     link_to(one_row.coordinator.last_name, :action => :coordinator)    
  #   end
  #
  # Helper Example when <tt>:type => :lookup</tt>:
  #
  #   def user_id__custom_select_options(one_row)
  #     UserLookup.find(:all, :include => :access_level, 
  #       :conditions => [ "access_level >= ?", AccessLevel::LEVELS[:boss] ], 
  #       :order => "last_name ASC, first_name ASC").collect { |u| [ (u.last_name[0..8] + ", " + 
  #                     u.first_name[0..2] + " (" + u.login[0..6] + ")"), u.id ] }    
  #   end
  #
  #   def user_id_custom_filter_field(one_row)
  #     UserLookup.find(:all, :include => :access_level, 
  #       :conditions => [ "access_level >= ?", AccessLevel::LEVELS[:boss] ], 
  #       :order => "last_name ASC, first_name ASC").collect { |u| [ (u.last_name[0..8] + ", " + 
  #                     u.first_name[0..2] + " (" + u.login[0..6] + ")"), u.id ] }    
  #   end
  #
  # Helper Examples when <tt>:type => :custom</tt>:
  #
  #   def name_key_custom_edit_field(one_row)
  #     select :one_row, :name_key , StaticString.find(:all, :conditions => [ "static_strings.string_key LIKE 'LOCALE%%'" ]).collect {|ss| [ ss.string_text, ss.string_key ] }
  #   end
  #
  #   def name_key_custom_list_field(one_row)
  #     one_row.name.string_text
  #   end
  #
  #   def name_key_custom_filter_field(filter_by)
  #     select filter_by, column.name , StaticString.find(:all, :conditions => [ "static_strings.string_key LIKE 'LOCALE%%'" ]).collect {|ss| [ ss.string_text, ss.string_key ] }, :include_blank => true
  #   end
  #
  # Controller Examples when providing default filter value or a restrict_to filter. These must go in the Controller or Model:
  #
  #   def user_id_custom_default_filter(one_row)
  #     current_user.role.access_level == AccessLevel::LEVELS[:admin] ? nil : current_user.id
  #   end
  #
  #   def event_custom_restrict_to_filter(one_row)
  #     build_clause(Event, 'user_id_2', current_user(User).id, :like => false) if current_user(User).role.access_level.to_s == AccessLevel::LEVELS[:client]
  #   end
  #
  #   def user_id_2_ajax_custom_calculate_filter(one_row)
  #     filter_fields = user_id_ajax_parse_into_parts(one_row[:user_id_2_ajax])
  #     build_clause(User, 'last_name', filter_fields[0], :like => true, :include => [:client_admin]) unless filter_fields[0].nil?
  #     build_clause(User, 'first_name', filter_fields[1], :like => true) unless filter_fields[1].nil?
  #   end
  #
  #   def user_id_ajax_parse_into_parts(filter_field)
  #     filter_fields = filter_field.downcase.split(",") unless filter_field.nil?
  #     filter_fields[0].strip! unless filter_fields[0].nil?
  #     filter_fields[1] = filter_fields[1].split(" ")[0] unless filter_fields[1].nil?
  #     filter_fields[1].strip! unless filter_fields[1].nil?
  #     filter_fields
  #   end
  #
  # Autocomplete example:
  #
  #   In the controller:
  #
  #   def auto_complete_for_filter_by_user_id_2_ajax
  #     filter_fields = user_id_ajax_parse_into_parts(params["#{params[:filter]}"]["user_id_2_ajax"])
  #     condition_clause = "access_level >= #{AccessLevel::LEVELS[:client]} and LOWER(last_name) LIKE ?"
  #     condition_clause << "and LOWER(first_name) LIKE ?" unless filter_fields[1].nil?
  #     conditions = [condition_clause]
  #     conditions << ['%' + filter_fields[0] + '%']
  #     conditions << ['%' + filter_fields[1] + '%'] unless filter_fields[1].nil?
  #     find_options = { 
  #       :conditions => conditions, 
  #       :include => :access_level,
  #       :order => "last_name, first_name ASC",
  #       :limit => 10 }
  #     @items = UserLookup.to_s.camelize.constantize.find(:all, find_options)  
  #     render :inline => "<%= auto_complete_result_user_id(@items) %>"
  #   end  
  #
  #   In the helper:
  #
  #   def auto_complete_result_user_id(entries)
  #     return unless entries
  #     items = entries.map { |entry| 
  #       content_tag("li", 
  #         content_tag('div', "#{entry[:last_name]}, #{entry[:first_name]}", :class => "name") + " " + 
  #         content_tag('div', "(#{entry[:login]})") #+
  #       )
  #     }
  #     content_tag("ul", items.uniq)
  #   end
  #
  def self.has_column(name, options = {})
    @scaffold_columns ||= []
    @scaffold_columns_hash ||= {}
    options[:name] = name
    if @scaffold_columns_hash[name].nil?
      scaffold_column = DryScaffold::ScaffoldColumn.new(self, options)
      @scaffold_columns << scaffold_column
      @scaffold_columns_hash[name] = scaffold_column
    end
  end
  # Accepts an array. See <tt>has_column</tt>.
  def self.has_columns(columns)
    columns.each do |column|
      has_column column[:name], column
    end
  end
  
  # Customize your the number of rows per page.
  #
  # Example in Model:
  #   show_per_page 20
  #
  # Example in environment.rb:
  #   module DryScaffoldEngine
  #     config :show_per_page, 20
  #   end  
  @default_per_page = nil
  def self.show_per_page(value)
    @default_per_page = value
  end
  
  def self.default_per_page #:nodoc:
    @default_per_page ||= DryScaffoldEngine.config(:show_per_page) if !DryScaffoldEngine.config(:show_per_page).nil?
    @default_per_page ||= 5
    @default_per_page
  end
  
  @scaffold_columns = nil
  def self.scaffold_columns #:nodoc:
    @scaffold_columns ||= build_scaffold_columns
  end
     
  @scaffold_columns_hash = nil
  def self.scaffold_columns_hash #:nodoc:
    @scaffold_columns_hash ||= build_scaffold_columns_hash
  end

  @child_models = nil
  def self.children #:nodoc:
    if @child_models.nil?
      []
    else
      @child_models
    end
  end

  # Formulates the relationships to be managed. 
  #
  # Because not all <tt>has_many</tt> or <tt>belongs_to</tt> relationships defined in the model are to be managed by DryScaffold, 
  # this method restricts the management to only those models identified.
  #
  # Example: 
  #   has_many :contacts, :dependent => :destroy
  #   has_many :comments, :dependent => :destroy
  #   has_child :contacts
  #   has_child :comments
  def self.has_child(model)
    # We store this as an array so that it can be ordered.
    @child_models ||= []
    case model
      when String
        model_const = model.camelize.singularize.constantize
      when Symbol
        model_const = model.to_s.camelize.singularize.constantize
      else
        model_const = model
    end
    if @child_models.index(model_const).nil?
      model_const.has_parent name
      @child_models << model_const
    end
  end
  
  # A shortcut method to <tt>has_child</tt>.
  #
  # Example: 
  #   has_many :contacts, :dependent => :destroy
  #   has_many :comments, :dependent => :destroy
  #   has_many :statuses, :dependent => :destroy
  #   has_children [:contacts, :comments, :statuses]
  def self.has_children(models = [])
    models.each do |model|
      has_child model
    end
  end

  def self.has_children? #:nodoc:
    not @child_models.nil?
  end

  @crud = nil
  # Primarily used to disable CRUD, sort, filter and paging actions Model-wide. This value will override 
  # any <tt>has_column</tt> values. Where applicable, it is also possible to specify a label other than the default.
  #
  # Options are:
  # * <tt>:creatable</tt> - Default value is +true+. Default label is +Create+.
  # * <tt>:deletable</tt> - Default value is +true+. Default label is +Delete+.
  # * <tt>:editable</tt> - Default value is +true+. Default label is +Edit+.
  # * <tt>:showable</tt> - Default value is +true+. Default label is +Show+.
  # * <tt>:filterable</tt> - Default value is +true+.
  # * <tt>:viewable</tt> - Default value is +true+. Default label is +View+.
  # * <tt>:pageable</tt> - Default value is +true+. (Display paging controls for this Model)
  # * <tt>:sortable</tt> - Default value is +true+.
  #
  # Example: 
  #   has_crudability :viewable => "Contacts", :deletable => false, :filterable => false
  def self.has_crudability(options)
    @crud = options
  end
  
  def self.crud #:nodoc:
    if @crud.nil?
      {}
    else
      @crud
    end
  end
  
  def self.is_crudable?(key) #:nodoc:
    if crud.has_key?(key) 
      crud[key]
    else
      true
    end
  end

  def self.crudable_label(key, default = "") #:nodoc:
    crud.has_key?(key) ? (crud[key].is_a?(String) ? crud[key] : default) : default
  end
  
  def self.filterable? #:nodoc:
    is_crudable?(:filterable)
  end
  
  def self.sortable? #:nodoc:
    is_crudable?(:sortable)
  end
  
  # At some level this just makes my life easier, I don't have to do the <tt>reflect_on_all_associations</tt> walk trying to
  # read your mind.
  #
  # Example: 
  #   belongs_to :company
  #   belongs_to :corporation
  #   has_parent :company
  def self.has_parent(model)
    @parent_model = model.to_s.camelize
  end
  
  def self.has_parent? #:nodoc:
    !@parent_model.nil?
  end
  
  def self.parent_model #:nodoc:
    @parent_model
  end
  
  def self.base_model #:nodoc:
    if @inherited.nil? or @inherited == false
      self.to_s.downcase.to_sym
		else
      self.to_s.camelize.constantize.superclass.to_s.underscore.to_sym
		end    
  end
  
  def self.url_model #:nodoc:
    if has_parent?
      @parent_model
    else
      name
    end
  end

  def self.url_sub_model #:nodoc:
    if has_parent?
      name
    else
      nil
    end
  end  

  # This is used by inline ajax edit
  def self.user_key #:nodoc:
    self.reflect_on_association(:user).nil? ? nil : self.reflect_on_association(:user).primary_key_name
  end
  
end
